﻿using System;
using System.Collections.Generic;
using System.Linq;

namespace Bettzueche.RLLibrary.Types
{
    /// <summary>
    /// A Tilie-Coding of one Dimension with multiple Tilings.
    /// </summary>
    /// <remarks>
    /// Splitting a dimension with range min to max into N tilings (sub-intervall organizations), where each
    /// tiling is a set of (connected) intervalls called tile, feature respectively.
    /// The precision is the smalles intersection of several intervalls and thus the maximum range
    /// between two values that have different feature sets.
    /// <para>Example of a Tiling-Organization:<code>
    ///  Min                   Max
    ///   .                     .
    ///   |---0----|---1----|   .
    ///   . |---2----|---3----| .
    /// |---4----|---5----|     .
    ///   .   |---6----|---7----|
    /// </code>
    /// </para>
    /// </remarks>
    public class MultiTiling
    {
        #region Fields

        /// <summary>Die einzelnen Tilings dieser Dimension</summary>
        Tiling[] _tilingsArr;

        /// <summary>Anzahl Tiles/Intervale je Tiling.</summary>
        int _tilesPerTiling;

        /// <summary>Indexwert bei dem angefangen werden soll zu zählen.</summary>
        int _tilesStartIndex;

        #endregion


        #region Properties

        /// <summary>
        /// Gets the (friendly) name of this Tiling.
        /// </summary>
        /// <value>
        /// The name.
        /// </value>
        public String Name { get; private set; }

        /// <summary>
        /// Gets the tiling count.
        /// </summary>
        /// <value>
        /// The tiling count.
        /// </value>
        public int TilingCount { get; private set; }

        /// <summary>
        /// Gets the overall tile count.
        /// </summary>
        /// <value>
        /// The tile count.
        /// </value>
        public int TileCount { get; private set; }

        /// <summary>
        /// Gets the minimum value (inclusive), that is in a tiling.<para>
        /// The feature number of values less than <c>Min</c> is <see cref="Bettzueche.RLLibrary.Types.Tiling#OutOfBounds">Tiling.OutOfBounds</see>.</para>
        /// </summary>
        /// <value>
        /// The minimum value (inclusive). Should be less than or equal to <c>Max</c>.
        /// </value>
        public double Min { get; private set; }

        /// <summary>
        /// Gets the maximum (exclusive), that is in a tiling.<para>
        /// The feature number of values greater than <c>Max</c> is <see cref="Bettzueche.RLLibrary.Types.Tiling#OutOfBounds">Tiling.OutOfBounds</see>.</para>.
        /// </summary>
        /// <value>
        /// The maximum value (exclusive). Should be greater than or equal to <c>Min</c>.
        /// </value>
        public double Max { get; private set; }

        /// <summary>
        /// Gets the precision of this MultiTiling
        /// which is the maximum range of two values with different feature sets.
        /// </summary>
        /// <value>
        /// The precision.
        /// </value>
        public double Precision { get; private set; }

        #endregion


        #region Constructor(s)

        /// <summary>
        /// Initializes a new instance of the <see cref="MultiTiling"/> class.<para>
        /// It's recommended to select <c>tilings</c>=b^n and <c>precision</c>=1/b^m with n&lt;=m.</para>
        /// </summary>
        /// <param name="tilings">The tilings count.</param>
        /// <param name="precision">The precision of this MultiTiling which is the maximum range of two values with different feature sets.
        ///   The precision should be a value resulting by an integer devision of the value range: (max - min) / n.</param>
        /// <param name="min">The minimum (inclusive), that is in a tiling.</param>
        /// <param name="max">The maximum (exclusive), that is in a tiling.</param>
        public MultiTiling(int tilings, double precision, double min, double max)
            :this(tilings, precision, min, max, 0, null)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="MultiTiling"/> class.
        /// </summary>
        /// <param name="tilings">The tilings count.</param>
        /// <param name="precision">The precision of this MultiTiling which is the maximum range of two values with different feature sets.
        ///   The precision should be a value resulting by an integer devision of the value range: (max - min) / n.</param>
        /// <param name="min">The minimum (inclusive), that is in a tiling.</param>
        /// <param name="max">The maximum (exclusive), that is in a tiling.</param>
        /// <param name="startIndex">The start index / first feature number (&grt;=0).</param>
        /// <param name="name">A friendly name for this MultiTiling (optional). Will be passed on to the 
        /// seperate Tilings.</param>
        public MultiTiling(int tilings, double precision, double min, double max, int startIndex, string name) {
            TilingCount = tilings;
            Precision = precision;
            Min = min;
            Max = max;
            _tilesStartIndex = startIndex < 0 ? 0 : startIndex;
            if (String.IsNullOrEmpty(name)) {
                Name = "T" + DateTime.Now.Millisecond.ToString();
            }
            Init();
        }

        #endregion


        #region Methods

        /// <summary>
        /// Gets the feature numbers the specified value has.<para>
        /// Array of length <see cref="TilingCount"/> with feature numbers for specified value.
        /// If the value is out of the bounds of one sub-tiling, the corresponding value is <c>Tiling.OutOfBounds</c>
        /// </para>
        /// </summary>
        /// <param name="value">The value.</param>
        /// <returns>Array of length <see cref="TilingCount"/> with feature numbers for specified value.
        /// If the value is out of the bounds of one sub-tiling, the corresponding value is <c>Tiling.OutOfBounds</c></returns>
        public int[] GetFeatureNumbers(double value) {
            // get Feature Numbers:..........................
            int[] featureVec = new int[TilingCount];
            for (int i = 0; i < _tilingsArr.Length; i++) {
                var tiling = _tilingsArr[i];
                int fNum = tiling.GetPartitionNumber(value);
                if (Tiling.OutOfBounds != fNum) {
                    fNum = i * _tilesPerTiling + fNum;  // [0,1,...] * 5 + [0,1,2,3,4]
                    fNum += _tilesStartIndex;
                }
                featureVec[i] = fNum;
            }
            return featureVec;
        }

        /// <summary>
        /// Gets the features the specified value has.
        /// </summary>
        /// <param name="value">The value.</param>
        /// <returns>Features for specified value.</returns>
        public Feature[] GetFeatureSet(double value) {
            int[] features = GetFeatureNumbers(value);
            Feature[] featureSet = new Feature[features.Length];
            for (int i = 0; i < features.Length; i++ ) {
                featureSet[i] = new Feature(features[i], true, 1); // TODO: macht nur für nicht-binäre Features Sinn
            }
            return featureSet;
        }

        /// <summary>
        /// Gets the medians of the specified partition numbers, that is the midpoint of the sub-interval (feature-range).
        /// </summary>
        /// <param name="features">The feature's indices.</param>
        /// <returns>Dictionary of feature range midpoints with feature index as key.</returns>
        public Dictionary<int, double> GetMedians(int[] features) {
            Dictionary<int, double> medians = new Dictionary<int, double>();
            for (int i = 0; i < features.Length; i++) {
                int tIdx = (features[i]-_tilesStartIndex) / _tilesPerTiling;
                var t = _tilingsArr[tIdx];
                int fIdx = (features[i]-_tilesStartIndex) - (tIdx * _tilesPerTiling);
                double median = t.GetMedian(fIdx);
                //if (Double.IsInfinity(median)) {
                //    continue;
                //}
                medians[features[i]] = median;
            }
            return medians;
        }

        /// <summary>
        /// Gets the medians of the specified partition numbers, that is the midpoint of the sub-interval (feature-range).
        /// </summary>
        /// <param name="features">The features.</param>
        /// <returns>Dictionary of feature range midpoints with feature index as key.</returns>
        public Dictionary<int, double> GetMedians(Feature[] features) {
            var fArr = from f in features
                        select f.ID;
            return GetMedians(fArr.ToArray());
        }

        /// <summary>
        /// Initializes this instance.
        /// </summary>
        /// <remarks>
        /// Erzeuge N Tilings mit je M Tiles. Das erste Tiling beginnt bei Min. jedes weitere Tiling wird
        /// abwechselnd und immer weiter nach rechts und links verschoben.
        /// </remarks>
        void Init() {
            double range = Max - Min;
            decimal intervallLength = (decimal)TilingCount * (decimal)Precision;// 3* 0.1 = 0.300000000000004
            
            _tilesPerTiling = (int)((decimal)range / intervallLength);
            TileCount = TilingCount * _tilesPerTiling;
            decimal maxTemp2 = (decimal)intervallLength * (decimal)_tilesPerTiling;
            double maxTemp = (double)maxTemp2;
            double offset = Precision; // = intervallLength / TilingCount;

            String tName = this.Name + "#";
            _tilingsArr = new Tiling[TilingCount];
            double sign = -1.0;
            for (int i = 0; i < TilingCount; i++) {
                double factor = Math.Ceiling(i / 2.0);
                double offs = sign * factor * offset;
                Tiling u = new Tiling(Min+offs, maxTemp+offs) { Name = tName + (i+1).ToString(), Partitions = _tilesPerTiling };
                _tilingsArr[i] = u;
                sign *= -1.0;
            }
        }

        #endregion

        /// <summary>
        /// Returns a <see cref="System.String" /> that represents this instance.
        /// </summary>
        /// <returns>
        /// A <see cref="System.String" /> that represents this instance.
        /// </returns>
        public override string ToString() {
            string s = String.Format("MultiTiling(Tilings:{0}|Tiles:{1}|Range:{2:N2}|Precision:{3:N3})", TilingCount, TileCount, Max - Min, Precision);
            return s;
        }
    }
}
